From 4a54e34cf5629f9fed61f0b7d69ee3ba4d874bc6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pali=20Roh=C3=A1r?= <pali@kernel.org>
Date: Sat, 9 Jul 2022 13:40:24 +0200
Subject: [PATCH] pppd: Add support for registering ppp interface via Linux
 rtnetlink API
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

pppd currently creates ppp network interface via PPPIOCNEWUNIT ioctl API.
This API creates a new ppp network interface named "ppp<unit_id>". If user
supply option "ifname" with custom network name then pppd calls SIOCSIFNAME
ioctl to rename "ppp<unit_id>" to custom name immediately after successful
PPPIOCNEWUNIT ioctl call. If custom name is already registered then
SIOCSIFNAME ioctl fails and pppd close current channel (which destroy also
network interface).

This has side effect that in the first few miliseconds interface has
different name as what user supplied.

Tools like systemd, udev or NetworkManager are trying to query
interface attributes based on interface name immediately when new
network interface is created.

But if interface is renamed immediately after creation then these tools
fails. For example when running pppd with option "ifname ppp-wan" following
error is reported by systemd / udev into dmesg log:

    [   35.718732] PPP generic driver version 2.4.2
    [   35.793914] NET: Registered protocol family 24
    [   35.889924] systemd-udevd[1852]: link_config: autonegotiation is unset or enabled, the speed and duplex are not writable.
    [   35.901450] ppp-wan: renamed from ppp0
    [   35.930332] systemd-udevd[1852]: link_config: could not get ethtool features for ppp0
    [   35.939473] systemd-udevd[1852]: Could not set offload features of ppp0: No such device

There is an easy way to fix this issue: Use new rtnetlink API.

Via rtnetlink API it is possible to create ppp network interface with
custom ifname atomically. Just it is not possible to specify custom ppp
unit id.

So use new rtnetlink API when user requested custom ifname without custom
ppp unit id. This will avoid system issues with interface renaming as ppp
interface is directly registered with specified final name.

This has also advantage that if requested interface name already exists
then pppd fail during registering of networking interface and not during
renaming network interface which happens after successful registration.

If user supply custom ppp unit id then it is required to use old ioctl API
as currently it is the only API which allows specifying ppp unit id.

When user does not specify custom ifname stay also with old ioctl API.
There is currently a bug in kernel which cause that when empty interface is
specified in rtnetlink message for creating ppp interface then kernel
creates ppp interface but with pseudo-random name, not derived from ppp
unit id. And therefore it is not possible to retrieve what is the name of
newly created network interface. So when user does not specify interface
name via "ifname" option (which means that want from kernel to choose some
"free" interface name) it is needed to use old ioctl API which do it
correctly for now.

Signed-off-by: Pali Rohár <pali@kernel.org>
---
 pppd/sys-linux.c | 194 ++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 192 insertions(+), 2 deletions(-)

--- a/pppd/sys-linux.c
+++ b/pppd/sys-linux.c
@@ -126,6 +126,11 @@
 #include <linux/netlink.h>
 #include <linux/rtnetlink.h>
 #include <linux/if_link.h>
+
+#ifdef INET6
+#include <linux/if_addr.h>
+#endif
+
 /* Attempt at retaining compile-support with older than 4.7 kernels, or kernels
  * where RTM_NEWSTATS isn't defined for whatever reason.
  */
@@ -135,16 +140,20 @@
 #define IFLA_STATS_LINK_64 1
 #endif
 
-#ifdef INET6
-#include <linux/if_addr.h>
 /* glibc versions prior to 2.24 do not define SOL_NETLINK */
 #ifndef SOL_NETLINK
 #define SOL_NETLINK 270
 #endif
+
 /* linux kernel versions prior to 4.3 do not define/support NETLINK_CAP_ACK */
 #ifndef NETLINK_CAP_ACK
 #define NETLINK_CAP_ACK 10
 #endif
+
+/* linux kernel versions prior to 4.7 do not define/support IFLA_PPP_DEV_FD */
+#ifndef IFLA_PPP_MAX
+/* IFLA_PPP_DEV_FD is declared as enum when IFLA_PPP_MAX is defined */
+#define IFLA_PPP_DEV_FD 1
 #endif
 
 #include "pppd.h"
@@ -657,6 +666,160 @@ void generic_disestablish_ppp(int dev_fd
 }
 
 /*
+ * make_ppp_unit_rtnetlink - register a new ppp network interface for ppp_dev_fd
+ * with specified req_ifname via rtnetlink. Interface name req_ifname must not
+ * be empty. Custom ppp unit id req_unit is ignored and kernel choose some free.
+ */
+static int make_ppp_unit_rtnetlink(void)
+{
+    struct {
+        struct nlmsghdr nlh;
+        struct ifinfomsg ifm;
+        struct {
+            struct rtattr rta;
+            char ifname[IFNAMSIZ];
+        } ifn;
+        struct {
+            struct rtattr rta;
+            struct {
+                struct rtattr rta;
+                char ifkind[sizeof("ppp")];
+            } ifik;
+            struct {
+                struct rtattr rta;
+                struct {
+                    struct rtattr rta;
+                    union {
+                        int ppp_dev_fd;
+                    } ppp;
+                } ifdata[1];
+            } ifid;
+        } ifli;
+    } nlreq;
+    struct {
+        struct nlmsghdr nlh;
+        struct nlmsgerr nlerr;
+    } nlresp;
+    struct sockaddr_nl nladdr;
+    struct iovec iov;
+    struct msghdr msg;
+    ssize_t nlresplen;
+    int one;
+    int fd;
+
+    fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
+    if (fd < 0) {
+        error("make_ppp_unit_rtnetlink: socket(NETLINK_ROUTE): %m (line %d)", __LINE__);
+        return 0;
+    }
+
+    /* Tell kernel to not send to us payload of acknowledgment error message. */
+    one = 1;
+    setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &one, sizeof(one));
+
+    memset(&nladdr, 0, sizeof(nladdr));
+    nladdr.nl_family = AF_NETLINK;
+
+    if (bind(fd, (struct sockaddr *)&nladdr, sizeof(nladdr)) < 0) {
+        error("make_ppp_unit_rtnetlink: bind(AF_NETLINK): %m (line %d)", __LINE__);
+        close(fd);
+        return 0;
+    }
+
+    memset(&nlreq, 0, sizeof(nlreq));
+    nlreq.nlh.nlmsg_len = sizeof(nlreq);
+    nlreq.nlh.nlmsg_type = RTM_NEWLINK;
+    nlreq.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL | NLM_F_CREATE;
+    nlreq.ifm.ifi_family = AF_UNSPEC;
+    nlreq.ifm.ifi_type = ARPHRD_NETROM;
+    nlreq.ifn.rta.rta_len = sizeof(nlreq.ifn);
+    nlreq.ifn.rta.rta_type = IFLA_IFNAME;
+    strlcpy(nlreq.ifn.ifname, req_ifname, sizeof(nlreq.ifn.ifname));
+    nlreq.ifli.rta.rta_len = sizeof(nlreq.ifli);
+    nlreq.ifli.rta.rta_type = IFLA_LINKINFO;
+    nlreq.ifli.ifik.rta.rta_len = sizeof(nlreq.ifli.ifik);
+    nlreq.ifli.ifik.rta.rta_type = IFLA_INFO_KIND;
+    strcpy(nlreq.ifli.ifik.ifkind, "ppp");
+    nlreq.ifli.ifid.rta.rta_len = sizeof(nlreq.ifli.ifid);
+    nlreq.ifli.ifid.rta.rta_type = IFLA_INFO_DATA;
+    nlreq.ifli.ifid.ifdata[0].rta.rta_len = sizeof(nlreq.ifli.ifid.ifdata[0]);
+    nlreq.ifli.ifid.ifdata[0].rta.rta_type = IFLA_PPP_DEV_FD;
+    nlreq.ifli.ifid.ifdata[0].ppp.ppp_dev_fd = ppp_dev_fd;
+
+    memset(&nladdr, 0, sizeof(nladdr));
+    nladdr.nl_family = AF_NETLINK;
+
+    memset(&iov, 0, sizeof(iov));
+    iov.iov_base = &nlreq;
+    iov.iov_len = sizeof(nlreq);
+
+    memset(&msg, 0, sizeof(msg));
+    msg.msg_name = &nladdr;
+    msg.msg_namelen = sizeof(nladdr);
+    msg.msg_iov = &iov;
+    msg.msg_iovlen = 1;
+
+    if (sendmsg(fd, &msg, 0) < 0) {
+        error("make_ppp_unit_rtnetlink: sendmsg(RTM_NEWLINK/NLM_F_CREATE): %m (line %d)", __LINE__);
+        close(fd);
+        return 0;
+    }
+
+    memset(&iov, 0, sizeof(iov));
+    iov.iov_base = &nlresp;
+    iov.iov_len = sizeof(nlresp);
+
+    memset(&msg, 0, sizeof(msg));
+    msg.msg_name = &nladdr;
+    msg.msg_namelen = sizeof(nladdr);
+    msg.msg_iov = &iov;
+    msg.msg_iovlen = 1;
+
+    nlresplen = recvmsg(fd, &msg, 0);
+
+    if (nlresplen < 0) {
+        error("make_ppp_unit_rtnetlink: recvmsg(NLM_F_ACK): %m (line %d)", __LINE__);
+        close(fd);
+        return 0;
+    }
+
+    close(fd);
+
+    if (nladdr.nl_family != AF_NETLINK) {
+        error("make_ppp_unit_rtnetlink: recvmsg(NLM_F_ACK): Not a netlink packet (line %d)", __LINE__);
+        return 0;
+    }
+
+    if ((size_t)nlresplen < sizeof(nlresp) || nlresp.nlh.nlmsg_len < sizeof(nlresp)) {
+        error("make_ppp_unit_rtnetlink: recvmsg(NLM_F_ACK): Acknowledgment netlink packet too short (line %d)", __LINE__);
+        return 0;
+    }
+
+    /* acknowledgment packet for NLM_F_ACK is NLMSG_ERROR */
+    if (nlresp.nlh.nlmsg_type != NLMSG_ERROR) {
+        error("make_ppp_unit_rtnetlink: recvmsg(NLM_F_ACK): Not an acknowledgment netlink packet (line %d)", __LINE__);
+        return 0;
+    }
+
+    /* error == 0 indicates success, negative value is errno code */
+    if (nlresp.nlerr.error != 0) {
+        /*
+         * Linux kernel versions prior to 4.7 do not support creating ppp
+         * interfaces via rtnetlink API and therefore error response is
+         * expected. On older kernel versions do not show this error message.
+         * When error is different than EEXIST then pppd tries to fallback to
+         * the old ioctl method.
+         */
+        errno = (nlresp.nlerr.error < 0) ? -nlresp.nlerr.error : EINVAL;
+        if (kernel_version >= KVERSION(4,7,0))
+            error("Couldn't create ppp interface %s: %m", req_ifname);
+        return 0;
+    }
+
+    return 1;
+}
+
+/*
  * make_ppp_unit - make a new ppp unit for ppp_dev_fd.
  * Assumes new_style_driver.
  */
@@ -676,6 +839,33 @@ static int make_ppp_unit(void)
 	    || fcntl(ppp_dev_fd, F_SETFL, flags | O_NONBLOCK) == -1)
 		warn("Couldn't set /dev/ppp to nonblock: %m");
 
+	/*
+	 * Via rtnetlink it is possible to create ppp network interface with
+	 * custom ifname atomically. But it is not possible to specify custom
+	 * ppp unit id.
+	 *
+	 * Tools like systemd, udev or NetworkManager are trying to query
+	 * interface attributes based on interface name immediately when new
+	 * network interface is created. And therefore immediate interface
+	 * renaming is causing issues.
+	 *
+	 * So use rtnetlink API only when user requested custom ifname. It will
+	 * avoid system issues with interface renaming.
+	 */
+	if (req_unit == -1 && req_ifname[0] != '\0' && kernel_version >= KVERSION(2,1,16)) {
+	    if (make_ppp_unit_rtnetlink()) {
+		if (ioctl(ppp_dev_fd, PPPIOCGUNIT, &ifunit))
+		    fatal("Couldn't retrieve PPP unit id: %m");
+		return 0;
+	    }
+	    /*
+	     * If interface with requested name already exist return error
+	     * otherwise fallback to old ioctl method.
+	     */
+	    if (errno == EEXIST)
+		return -1;
+	}
+
 	ifunit = req_unit;
 	x = ioctl(ppp_dev_fd, PPPIOCNEWUNIT, &ifunit);
 	if (x < 0 && req_unit >= 0 && errno == EEXIST) {
